Multi-body bookings: divisional and multi-body visibility scopes (prod) - #22
Merged
Conversation
Bookings and room_requests gain a `scope` of single | divisional | multi. `body_id` stays populated in every case as the originating body, so all 48 existing bookings and 13 requests remain valid with no backfill, and audit log / email attribution never depends on the scope. - schema: scope + division columns, booking_bodies and room_request_bodies join tables, CHECK constraints, partial indexes on the divisional path. - rls: generalizes the policies that previously resolved visibility through a single body_id. booking_is_visible / booking_is_manageable are now the single definition, delegated to by every child-table policy so the rule cannot drift. Also pins search_path on the three pre-existing helpers, clearing a live function_search_path_mutable finding. - grants: new helpers are internal RLS predicates, not API surface, so EXECUTE is revoked from PUBLIC/anon. `authenticated` must keep it -- verified that policy evaluation itself requires EXECUTE on functions the policy calls. Includes a rollback script captured verbatim from pg_policies before the rewrite, so the prior behavior can be restored exactly rather than from memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds lib/booking-scope.ts as the single app-side home for the scope rule, mirroring booking_is_visible / booking_is_manageable in SQL. RLS only guards reads here -- every booking and request write goes through the service-role client, which bypasses RLS entirely -- so the guards in this module are the real write authorization. Shipping the migration without these would let leadership request a divisional booking for a division they do not lead. - replaces the single-body Leadership check duplicated across the revision, cancellation and request routes with requireBookingManager. - the three admin booking routes persist scope and sync booking_bodies on both create and edit, clearing the join rows when the scope moves away from multi. - resolveBookingRecipients replaces six per-route membership queries that only ever notified the owning body, which would under-notify divisional and multi bookings. Multi notifies all listed bodies; divisional notifies the owning body plus peer leadership, so a whole division is not emailed on every edit. - my-rooms can no longer express visibility as one .in(body_id): PostgREST has no subquery syntax, so the divisional and multi paths are resolved to ids first and folded into a single .or(). canManage is now computed server-side per booking instead of derived client-side from a flat leadership list, which only ever worked for single-body bookings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BookingScopeSelector replaces the plain Body <select> that the three admin create forms, three admin edit forms, and the leadership request page each declared inline. Single body stays the default and still reads as one field, so the common case is unchanged. Admins may scope to any division; leadership only to divisions they lead, and the option is disabled with a hint when they lead none. The multi-body pool is every active body for everyone, since any leadership may request a multi-body booking with any combination -- that leaks nothing, as bodies are already readable by any authenticated user. Switching scope clears the fields the new scope does not use, so the payload always satisfies the database CHECK constraints. ScopeLabel renders "Campus Affairs (Division)" and "DEI Committee + 2 others" with the full list on hover and click-to-expand. My Rooms now groups by scope rather than by body, since for divisional and multi bookings the owning body is not what determines who sees them. public/sw.js is the regenerated next-pwa artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`supabase db dump` writes data.sql / roles.sql / schema.sql into the repo root. These should never be committed: a populated roles.sql can contain role passwords and data.sql is a full copy of production. Schema history belongs in supabase/migrations/. The three currently present are 0 bytes, from an interrupted dump. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves two conflicts in My Rooms, where the #14/#13/#16 rework landed on the same files as the scope changes. - app/api/my-rooms/route.ts: kept the scope-aware visibility rewrite (the three-way OR across body / division / booking_bodies, and server-computed canManage) and folded in dev'"'"'s senate_type_preferences fetch and return. The old leadershipBodyIds response field stays dropped -- it only ever described single-body bookings. - app/(dashboard)/my-rooms/page.tsx: took dev'"'"'s reworked page as the base and reapplied the scope changes onto it, rather than the reverse, so the new list/calendar toggle, search, status filter and Senate preferences are all preserved. FlatBooking now lives in dev'"'"'s shared.ts, so canManage / scopeKey / scopeLabel and the scopeKeyOf / scopeLabelOf helpers moved there too -- calendar-view and booking-detail-modal consume the same type and now show the scope label rather than the owning body, matching the rest of My Rooms. Their #15 bcc fix composes well with the wider divisional/multi fan-out: the larger recipient list is no longer exposed via to:/cc:. tsc clean, build succeeds, and the only lint findings in the touched area are pre-existing ones in notification-bell.tsx and LoginCard.tsx. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
booking_bodies and room_request_bodies were created with a composite (parent_id, body_id) primary key -- the textbook junction shape. PostgREST detects that and began inferring a many-to-many `bookings <-> bodies` relationship through booking_bodies, on top of the existing many-to-one bookings.body_id -> bodies.id. With two candidate relationships every `bodies(name)` embed became ambiguous and PostgREST answered 300 / PGRST201. That broke reads app-wide, including code that predates this feature -- creating the tables was enough on its own. The Administrator and My Rooms pages showed "no bookings found" while all 48 bookings were still present. board_memberships is the precedent already in this schema: also a junction table, but with a surrogate id primary key and the pair merely UNIQUE, which is why users <-> bodies has never been ambiguous. The junction tables now match it. The unique constraint preserves the real invariant; only the backing index changes. Also stops the two affected read routes from coercing a failed query into an empty list. `data || []` is what turned a hard API error into a calm "no bookings found" -- the failure was indistinguishable from having no data, which is why this looked like missing rows rather than a broken query. Verified over the REST API that both the pre-existing `bodies(name)` embeds and the new nested ones return 200, and re-ran the RLS persona matrix to confirm the primary key change did not affect visibility. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A hidden booking is only visible to those who can manage it, but resolveBookingRecipients never consulted `hidden` -- so every member of the audience got a user_alert and an email whenever one was updated, telling them about a booking they cannot see. Hidden now narrows recipients to Leadership across the whole audience, for all three scopes, which is exactly the set canManageScoped() admits. Confirmed against real data: a hidden Campus Affairs divisional booking previously notified 7 people including 1 plain member, and now notifies the 6 leadership. `hidden` is looked up inside resolveBookingRecipients rather than passed in by each route, because a caller forgetting to pass it is precisely what caused this bug. One lookup, no route can bypass it. Visibility itself was already correct and needed no change: my-rooms filters on `!hidden || canManage`, and canManage is scope-aware, so members are excluded from hidden single-body, divisional and multi bookings alike. Bumps the version to 1.13.0 and moves the FAQ roadmap heading to v1.14.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Multi-body bookings: divisional and multi-body visibility scopes (#19)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See PR to dev